iT邦幫忙

2024 iThome 鐵人賽

DAY 6
1
Modern Web

Svelte 的奇妙冒險系列 第 6

[Svelte 的奇妙冒險] Day 06 - 深入 $effect

  • 分享至 

  • xImage
  •  

與生命週期(lifecycle)的關係

因為 Svelte 還擁有 lifecycle fucntion ,所以我們不必為了模擬生命週期而使用 $effect ,而它們基本上在某些情況中可以說是一樣的東西,所以就只是開發時要想什麼時候要使用 lifecycle 或者 $effect 會比較好寫而已 。

目前 Svelte 有四個 lifecycle function ,分別是 onMountbeforeUpdateafterUpdateonDestroy ,但在 Svelte 5 後基本上 beforeUpdateafterUpdate 都可以用 $effect.pre$effect 所代替。

<!-- in Counter.svetle -->
<script lang="ts">
	import { onDestroy, onMount } from 'svelte';

	let obj = $state({ value: 0 });

	let derivedObj = $derived({ value: obj.value * 2 });
	let p: HTMLParagraphElement | null = $state(null);

	$effect.pre(() => {
		console.log(
			'\x1b[36m%s\x1b[0m',
			`[Pre Effect]\n`,
			`p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
		);
		return () => {
			console.log(
				'\x1b[36m%s\x1b[0m',
				`[Pre Effect Cleanup]\n`,
				`p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
			);
		};
	});

	$effect(() => {
		console.log(
			'\x1b[32m%s\x1b[0m',
			`[Effect 1]\n`,
			`p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
		);
		return () => {
			console.log(
				'\x1b[32m%s\x1b[0m',
				`[Effect 1 Cleanup]\n`,
				`p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
			);
		};
	});

	onMount(() => {
		console.log(
			'\x1b[33m%s\x1b[0m',
			`[onMount]\n`,
			`p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
		);
	});

	onDestroy(() => {
		console.log(
			'\x1b[31m%s\x1b[0m',
			`[onDestroy]\n`,
			`p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
		);
	});
</script>

<button onclick={() => (obj.value += 1)}> Increment obj.value </button>
<button
	onclick={() =>
		(obj = {
			...obj,
			value: obj.value + 1
		})}
>
	Increment obj.value (immutable)</button
>

<p class="content" bind:this={p}>{obj.value} doubled is {derivedObj.value}</p>


onMount

初次掛載的輸出是這樣的:

看得出來 onMount 的執行時機是在 DOM 完成掛載後後,而且看這個輸出順序代表onMount 是會在 $effect 第一次執行完才執行嗎?其實不是

onMount(() => {
		console.log(
			`\x1b[33m%s\x1b[0m`,
			`[onMount]\n`,
			`p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
		);
	});

  $effect(() => {
      console.log(
          '\x1b[32m%s\x1b[0m',
          `[Effect 1]\n`,
          `p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
      );
      return () => {
          console.log(
              '\x1b[32m%s\x1b[0m',
              `[Effect 1 Cleanup]\n`,
              `p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
          );
      };
  });

我們把 onMount 移到 $effect 前就會發現輸出變成

沒錯其實 $effect 的第一次執行跟 onMount 是一樣的東西。可以把 onMount 想成一種只執行一次 $effect ,已知 $effect 就是在 DOM 完成渲染後才會執行,所以這時候純粹就是先執行誰的 effect 的事情而已。

onDestroy

onDestroy 也很好理解了,就是我們將 component destroy 時會觸發且也可以把它想像成想成一種只執行一次 $effect 但只有 cleanup 的部分,所以也是先執行 $effect.pre 的 cleanup 後再執行 $effect 的 cleanup 及 onDestroy

看到這裡可能會有疑惑為什麼 onDestroy 或者 $effect 的 cleanup 的 p?.innerText 是有值的?理論上他們執行時機是畫面更新後所以應該會是 undfined 才對,這邊我自己的理解是 destory 是一個特殊的行為,所以他們在 destory 會在 component 真正從 DOM node 被移除之前就先動了。

但 Svelte 提供了一個叫做 tick 的 function ,它會回傳一個 promise 直到任意狀態被改變才會被 resolve 或者沒有狀態的情況下會在下一個 microtask 中 resolve

import { tick } from 'svelte';

onDestroy(() => {
		tick().then(() => {
			console.log(
				'\x1b[31m%s\x1b[0m',
				`[onDestroy]\n`,
				`p?.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
			);
		});
	});

當我們使用 tick 後他就會直到下一個狀態改變後才會 resolve ,然後才會我們 .then 裡面的 console.log

那我們再來統整一次 $effect$effect.preonMountonDestory 的執行順序

  1. 初始狀態: $effect.pre → DOM node 掛載完畢 →onMount / $effect

  2. 狀態更新:$effect.pre 的cleanup → $effect.pre → DOM node 更新完畢(如果需要)→ $effect 的 cleanup → $effect

  3. 移除 component : $effect.pre 的cleanup → $effect 的 cleanup → onDestroy

untrack

這幾天一直在說的 Svelte 會自動追蹤依賴然後依賴更新就會更新狀態或執行 effect,那假設有些情況就是我需要在 A 狀態變更下去看 B 狀態的值但我 B 狀態更新時並不想觸發 effect 呢?這時可以使用 untrack ,這個 function 就是告訴 Svelte 說這個依賴不要被自動追蹤。

以這個 Counter 來說我並不在意第一次渲染時 p 被更新時也去觸發 $effect.pre 但我依然想在 $effect.pre 讀取他的值。

import {  untrack } from 'svelte';

$effect.pre(() => {
    console.log(
        '\x1b[36m%s\x1b[0m',
        `[Pre Effect]\n`,
        `p.innerText: ${untrack(() => p?.innerText)} \n obj.value: ${obj.value}`
    );

可以看到初次掛載後就不會有 pundefined 更新後的 $effect.pre 和它的 cleanup 的 console.log 了。

所以甚至我可以讓一個 $effect 所有的依賴都被 untrack 那基本上就跟 onMount 沒兩樣了。

什麼時候不要用 $effect

$effect 很好用但也很容易被濫用,甚至大多數情況下可以不必使用 $effect 也能達成需求。

就像是 React 的 useEffect 一樣

只有第一次渲染時會執行到的 side-effect

很明顯的如果這種情況直接使用 onMount 就好,除非真的是想要在 DOM 掛載前就先執行 effect 才會去使用 $effect.pre

狀態是依賴於另外一個狀態時

像這種情況可以直接改用 $dervied 就好

// ❌ 不要這樣寫
let count = $state(0)
let double = $state(0)

$effect(()=>{
  double = count * 2
})

或許可能會想說有些值很複雜不是一個簡單的值就能表示的 $derived ,那這時可以使用 $derived.by 可以傳入一個 function 最後 return 的值就是要改變的值。

// 這兩個是一樣的作用
let double = $derived( count * 2 )
let double = $derived.by(()=> count * 2 )

更新狀態時需要順便更新另外一個狀態時

這邊直接沿用官方文件的範例,這邊會看到兩個 state : spentleft ,功能是不管我是改動 spent 還是 left 另外一個都要隨之更新。

<script>
	let total = 100;
	let spent = $state(0);
	let left = $state(total);

	$effect(() => {
		left = total - spent;
	});

	$effect(() => {
		spent = total - left;
	});
</script>

<label>
	<input type="range" bind:value={spent} max={total} />
	{spent}/{total} spent
</label>

<label>
	<input type="range" bind:value={left} max={total} />
	{left}/{total} left
</label>

<style>
	label {
		display: flex;
		gap: 0.5em;
	}
</style>

bind:value 簡單解釋就是雙向綁定的語法糖,也就是讓 input 的 value 可以影響 state , state 也可以影響 input 的 value,也可以理解為 React 的 controlled component。

不推薦的原因是不管我是選擇去變更哪一個狀態這兩個 $effect 都會被執行,但我們其實預期的是假設我只變更 spent 那應該只有 $effect 第一個執行然後去計算 left 就好。但實際上會變成

更新 spent → effect 更新了 leftleft 更新了所以觸發了 effect 去更新 spent,不能看出可能會有無限迴圈的問題,也剛好這個例子不會發生。

所以文件給出了一個修改的提案:使用 event 更改狀態

<script>
	let total = 100;
	let spent = $state(0);
	let left = $state(total);

	function updateSpent(e) {
		spent = +e.target.value;
		left = total - spent;
	}

	function updateLeft(e) {
		left = +e.target.value;
		spent = total - left;
	}
</script>

<label>
	<input type="range" value={spent} oninput={updateSpent} max={total} />
	{spent}/{total} spent
</label>

<label>
	<input type="range" value={left} oninput={updateLeft} max={total} />
	{left}/{total} left
</label>

<style>
	label {
		display: flex;
		gap: 0.5em;
	}
</style>

今天總算把比較常用 rune 都介紹過一輪了,當然目前 rune 不只這只有這些只是只會在某些特殊場合才會需要,就當未來有用到時在特別提出來介紹吧。

參考資料

source code

https://github.com/toddLiao469469/30days-for-svelte5/tree/main/src/routes/day06


上一篇
[Svelte 的奇妙冒險] Day 05 - $effect 的基本用法
下一篇
[Svelte 的奇妙冒險] Day 07 - 常用的 directives
系列文
Svelte 的奇妙冒險30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言